/*-
 *******************************************************************************
 * Copyright (c) 2011, 2016 Diamond Light Source Ltd.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Matthew Gerring - initial API and implementation and/or initial documentation
 *******************************************************************************/
package org.eclipse.dawnsci.nexus.validation;

import java.text.MessageFormat;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.measure.unit.Unit;

import org.eclipse.dawnsci.analysis.api.metadata.UnitMetadata;
import org.eclipse.dawnsci.analysis.api.tree.Attribute;
import org.eclipse.dawnsci.analysis.api.tree.GroupNode;
import org.eclipse.dawnsci.nexus.NXobject;
import org.eclipse.dawnsci.nexus.NXtransformations;
import org.eclipse.january.dataset.Dataset;
import org.eclipse.january.dataset.IDataset;
import org.eclipse.january.metadata.MetadataType;

/**
 * Abstract superclass for Nexus application definition validators.
 * The logic for validating that nodes are not null, that fields and attributes
 * have the correct type etc. are in this abstract superclass.
 * A subclass exists for each application definition, generated by XSLT. Each subclass
 * invokes the methods in this abstract superclass as appropriate according to the
 * application definition as defined by the appropriate NXDL file.
 */
public abstract class AbstractNexusValidator implements NexusApplicationValidator {
	
	private Map<String, Integer> globalDimensionPlaceholderValues = new HashMap<>();
	
	private Map<String, Integer> localGroupDimensionPlaceholderValues = new HashMap<>();
	
	/**
	 * Throw an {@link NexusValidationException} with the given message.
	 * @param message message
	 * @throws NexusValidationException always
	 */
	protected void failValidation(final String message) throws NexusValidationException {
		if (message == null) {
			throw new NexusValidationException(null);
		} else {
			throw new NexusValidationException(message);
		}
	}
	
	/**
	 * Validates that the given condition holds, throwing an {@link NexusValidationException} with the given message otherwise
	 * @param message message
	 * @param condition condition to check
	 * @throws NexusValidationException if the condition does not hold
	 */
	protected void validate(String message, boolean condition) throws NexusValidationException {
		if (!condition) {
			failValidation(message);
		}
	}
	
	/**
	 * Validates that the given object is not <code>null</code>.
	 * @param message message
	 * @param object object to check for <code>null</code>
	 * @throws NexusValidationException if the given object is <code>null</code>
	 */
	protected void validateNotNull(String message, Object object) throws NexusValidationException {
		validate(message, object != null);
	}
	
	/**
	 * Validates that the given group is not null.
	 * @param groupName name of group
	 * @param type type of group
	 * @param groupNode group node
	 * @throws NexusValidationException
	 */
	protected void validateGroupNotNull(String groupName, Class<? extends NXobject> type, GroupNode groupNode) throws NexusValidationException {
		validateNotNull((groupName == null ? "The unnamed group " : "The group '" + groupName + "' ") + "of type " + type.getSimpleName() + " must not be null", groupNode);
	}
	
	/**
	 * Validates that the given field value is not <code>null</code>.
	 * @param fieldName name of field
	 * @param dataset the field value, an {@link IDataset}
	 * @throws NexusValidationException if the field is <code>null</code>
	 */
	protected void validateFieldNotNull(String fieldName, IDataset dataset) throws NexusValidationException {
		validateNotNull("The field " + fieldName + " must be set", dataset);
	}
	
	/**
	 * Validates that the given attribute node is not <code>null</code>.
	 * @param attributeName name of attribute
	 * @param attribute attribute 
	 * @throws NexusValidationException if the attribute is <code>null</code>
	 */
	protected void validateAttributeNotNull(String attributeName, Attribute attribute) throws NexusValidationException {
		validateNotNull("The attribute " + attributeName + " must be set", attribute);
		validateNotNull("The dataset for the attribute " + attributeName + " must be set", attribute.getValue());
	}
	
	/**
	 * Validates that an enumeration field has one of the given permitted values.
	 * @param fieldName name of the field
	 * @param dataset the field value, a {@link Dataset}
	 * @param permittedValues the permitted values
	 * @throws NexusValidationException if the value of the field is not one of the permitted values
	 */
	protected void validateFieldEnumeration(String fieldName, IDataset dataset, String... permittedValues) throws NexusValidationException {
		validateEnumeration(fieldName, "field", dataset, permittedValues);
	}
	
	/**
	 * Validates that the type of the given field is that given.
	 * @param fieldName field name
	 * @param dataset field value, an {@link IDataset}
	 * @param type expected type
	 * @throws NexusValidationException if the type of the field is not that given
	 */
	protected void validateFieldType(final String fieldName, final IDataset dataset, final NexusDataType type) throws NexusValidationException {
		type.validate(fieldName, dataset);
	}
	
	/**
	 * Validates that the given field has units consistent with the given unit category.
	 * 
	 * @param fieldName field name
	 * @param dataset field value, an {@link IDataset}
	 * @param unitCategory expected unit category
	 * @throws Exception if an unexpected exception occurs
	 * @throws NexusValidationException if the field's units are not consistent with the given unit category
	 */
	protected void validateFieldUnits(final String fieldName, final IDataset dataset,
			final NexusUnitCategory unitCategory) throws NexusValidationException {
		List<? extends MetadataType> metadata;
		try {
			metadata = dataset.getMetadata(UnitMetadata.class);
		} catch (Exception e) {
			throw new NexusValidationException("Could not get unit metadata for field '" + fieldName + "'", e);
		}
		// TODO why does getMetadata return a list? Can I assume I'm only interested in the first element?
		if (metadata == null || metadata.isEmpty() || !metadata.get(0).getClass().equals(UnitMetadata.class)) {
			failValidation("No unit metadata for field '" + fieldName + "', expected " + unitCategory);
		}
		
		if (metadata.size() > 1) {
			failValidation("Multiple unit metadata items found for field '" + fieldName + "'");
		}
		
		Unit<?> unit = ((UnitMetadata) metadata.get(0)).getUnit();
		if (!unitCategory.isCompatible(unit)) {
			failValidation("Unit " + unit + " is not compatible with the unit category " + unitCategory);
		}
	}

	/**
	 * Validates that the given field has the expected rank.
	 * @param fieldName field name
	 * @param dataset field value, an {@link IDataset}
	 * @param rank expected rank
	 * @throws NexusValidationException if the field does not have the expected rank
	 */
	protected void validateFieldRank(final String fieldName, final IDataset dataset, final int rank)
			throws NexusValidationException {
		if (dataset.getRank() != rank) {
			failValidation("The field " + fieldName + " has a rank of " + dataset.getRank() + ", expected " + rank); 
		}
	}
	
	/**
	 * Validate the dimensions of the given field.
	 * @param fieldName field name
	 * @param dataset dataset to validate
	 * @param groupName the name of the group
	 * @param dimensions the dimensions, each value must be either an integer, interpreted as the expected size of
	 *    that dimension, or a placeholder string, in which case the size of this dimension will be validated
	 *    against any previous dimension with the same placeholder string
	 * @throws NexusValidationException if a dimension did not have the expected size
	 */
	protected void validateFieldDimensions(final String fieldName,
			final IDataset dataset, String groupName, Object... dimensions)
			throws NexusValidationException {
		final int[] shape = dataset.getShape();

		for (int i = 0; i < dimensions.length; i++) {
			if (dimensions[i] instanceof Integer) {
				// the dimension value to validate against in an integer specifying exactly the expected dimension size to check
				if (shape[i] != ((Integer) dimensions[i]).intValue()) {
					failValidation(MessageFormat.format("The dimension with index {0} of field ''{1}'' expected to have size {2} was {3}", 
							(i + 1), fieldName, dimensions[i], shape[i]));
				}
			} else if (dimensions[i] instanceof String) {
				// the dimension value to validate against is a string placeholder
				// if the name of the group is specified, then this is defined in the NXDL for the base class for that group type
				// otherwise the placeholder is global across the whole application
				
				// we need to check that all dimensions (across the group or application depending on whether there is a group name)
				// have the same size. To do this, if this is the first time we've seen this placeholder we store the size of the
				// current dimension. On subsequent encounters, we check that the current dimension has the same size as this
				// stored value
				final String dimensionPlaceholder = (String) dimensions[i];
				Integer expectedSize = getDimensionPlaceholderValue(dimensionPlaceholder, groupName != null, shape[i]);
				if (expectedSize != null && shape[i] != expectedSize.intValue()) {
					if (groupName != null) {
						failValidation(MessageFormat.format("The dimension with index {0} of field ''{1}'' expected to have size {2} according to symbol ''{3}'' within group {4}, was {5}",
								(i + 1), fieldName, expectedSize, dimensions[i], groupName, shape[i]));
					} else {
						failValidation(MessageFormat.format("The dimension with index {0} of field ''{1}'' expected to have size {2} according to symbol ''{3}'', was {4}",
								(i + 1), fieldName, expectedSize, dimensions[i], shape[i]));
					}
				}
			} else {
				failValidation("Dimension size value must be an Integer or String, was: " + dimensions[i].getClass().getName());
			}
		}
	}
	
	/**
	 * Validates that the type of the given attribute is that given.
	 * @param fieldName field name
	 * @param dataset field value, an {@link IDataset}
	 * @param type expected type
	 * @throws NexusValidationException if the type of the field is not that given
	 */
	protected void validateAttributeType(final String fieldName, final Attribute attribute, final NexusDataType type) throws NexusValidationException {
		type.validate(fieldName, attribute.getValue());
	}

	/**
	 * Validates that an enumeration attribute has one of the given permitted values.
	 * @param attributeName name of the attribute
	 * @param attribute the attribute
	 * @param permittedValues the permitted values
	 * @throws NexusValidationException if the value of the field is not one of the permitted values
	 */
	protected void validateAttributeEnumeration(String attributeName, Attribute attribute, String... permittedValues) throws NexusValidationException {
		validateEnumeration(attributeName, "attribute", attribute.getValue(), permittedValues);
	}

	/**
	 * Validate the given transformations. Transformations have an order, whereby the initial dependsOnStr
	 * identifies the first transformation, and thereafter each transformation is identified by the
	 * value of the <code>depends_on</code> attribute of the previous transformation. The 
	 * final transformation is identified by having <code>"."</code> as the value of its depends_on attribute. 
	 * @param transformations transformations
	 * @param dependsOnStr the name of the first transformation
	 * @throws NexusValidationException if an expected transformation does not exist
	 */
	protected void validateTransformations(final Map<String, NXtransformations> transformations, String dependsOnStr) throws NexusValidationException {
		final Set<String> encounteredTransformationNames = new HashSet<String>();
		do {
			// get the tranformation with the given name
			final NXtransformations transformation = transformations.get(dependsOnStr);
			
			// check that the transformation exists
			if (transformation == null) {
				failValidation("No such transformation: " + dependsOnStr);
			}
			
			// check we haven't already encountered this transformation, if so the
			// transformations have a circular dependency
			if (encounteredTransformationNames.contains(dependsOnStr)) {
				failValidation("Circular dependency detected in transformations, transformation '" + dependsOnStr + "' encountered for second time.");
			}
			encounteredTransformationNames.add(dependsOnStr);
			Attribute dependsOnAttr = transformation.getAttribute("depends_on");
			dependsOnStr = (dependsOnAttr == null ? null : dependsOnAttr.getFirstElement());
		} while (dependsOnStr != null && !dependsOnStr.equals(".")); // "." marks the final transformation
	}
	
	/**
	 * Clears the map of values of dimension placeholders, as these are local only to the current group.
	 */
	protected void clearLocalGroupDimensionPlaceholderValues() {
		localGroupDimensionPlaceholderValues = new HashMap<String, Integer>();
	}

	private void validateEnumeration(String nodeName, String nodeType, IDataset dataset, String... permittedValues) throws NexusValidationException {
		// note: this method assumes that the enumeration values are always strings
		if (dataset.getRank() != 1) { // TODO confirm rank for enums: 0 or 1?
			failValidation(MessageFormat.format("The enumeration {0} ''{1}'' must have a rank of 1", nodeType, nodeName));
		}
		
		// the size of the field must be 1
		if (dataset.getSize() != 1) {
			failValidation(MessageFormat.format("The enumeration {0} ''{1}'' must have a size of 1", nodeType, nodeName));
		}
		
		String value = dataset.getString(0);
		validateNotNull(MessageFormat.format(
				"The value of the enumeration {0} ''{1}'' cannot be null", nodeType, nodeName), value);
		
		boolean valuePermitted = false;
		for (String permittedValue : permittedValues) {
			if (value.equals(permittedValue)) {
				valuePermitted = true;
				break;
			}
		}
		
		if (!valuePermitted) {
			failValidation(MessageFormat.format(
					"The value of the {0} ''{1}'' must be one of the enumerated values.", nodeType, nodeName));
		}
	}

	/**
	 * A helper method to get the actual dimension size for the given placeholder string, if it exists.
	 * If this is the first occurrence of this placeholder, 
	 * @param placeholder
	 * @param local
	 * @param actualDimensionSize
	 * @return the dimension placeholder value
	 */
	private Integer getDimensionPlaceholderValue(String placeholder, boolean local, int actualDimensionSize) {
		final Integer dimensionPlaceholderValue;
		if (local) {
			dimensionPlaceholderValue = localGroupDimensionPlaceholderValues.get(placeholder);
			if (dimensionPlaceholderValue == null) {
				localGroupDimensionPlaceholderValues.put(placeholder, actualDimensionSize);
			} else {
				
			}
		} else {
			dimensionPlaceholderValue = globalDimensionPlaceholderValues.get(placeholder);
			if (dimensionPlaceholderValue == null) {
				globalDimensionPlaceholderValues.put(placeholder, actualDimensionSize);
			}
		}
		
		return dimensionPlaceholderValue;
	}
	
}
